/*
 * Project:     ARISE Serial Device Library
 * Authors:     Jeffrey Arcand <jeffrey.arcand@ariselab.ca>
 * File:        ca/ariselab/lib/serialdevices/SerialModule.java
 * Date:        Wed 2011-04-27
 * License:     GNU General Public License version 2
 */
 
package ca.ariselab.lib.serialdevices;
 
import gnu.io.CommPort;
import gnu.io.CommPortIdentifier;
import gnu.io.NoSuchPortException;
import gnu.io.PortInUseException;
import gnu.io.SerialPort;
import gnu.io.UnsupportedCommOperationException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;


/**
 * A SerialModule is an object that can be used to represent a physical
 * electronic device attached to the computer via a serial port.  This object,
 * when extended, will have all the state information of the physical devices
 * stored in its member variables.  The serial device sends its updated state
 * information in burst messages that are automatically sent by the device and
 * need to be parsed by overriding the readUpdatesFromDevice() method in the
 * child class.  The serial device receives its updated state information in
 * burst messages automatically sent by this class which are generated by
 * overriding the sendUpdatesToDevice() method in the child class. 
 * @author Jeffrey Aarcand <jeffrey.arcand@ariselab.ca>
 */
public abstract class SerialModule {
	
	private CommPortIdentifier port;
	
	private int portSpeed;
	
	/** The port that is used to communicate with the physical device. */
	protected SerialPort serialPort;
	
	/** The looping threads that make sure to read the messages sent by the
	 * device and send the messages to be read by the device. */
	private SerialLoopingThread readingThread, writingThread;
	
	/** The input stream of the serial port. */
	private InputStream ins;
	
	/** The output stream of the serial port. */
	private OutputStream outs;
	
	/** Whether or not the serial module has been closed. */
	private boolean closed = false;
	
	/**
	 * Create a new SerialModule that corresponds to the serial device with the
	 * specified ID.
	 * @param devId The ID of the desired serial device.
	 * @throws SerialDeviceInitException When there is a problem connecting to
	 * the serial device.
	 */
	public SerialModule(SerialDeviceID devId) throws SerialDeviceInitException {
		this(SerialPortMgmt.getPortByDeviceId(devId, true)); //TODO
	}
	
	/**
	 * Create a new SerialModule that corresponds to the serial device on the
	 * specified serial port.
	 * @param portName The text name of the desired serial port.
	 * @throws SerialDeviceInitException When there is a problem connecting to
	 * the serial device.
	 */
	public SerialModule(String portName) throws SerialDeviceInitException {
		this(getPortIdentifier(portName));
	}
	
	/**
	 * Create a new SerialModule that corresponds to the serial device on the
	 * provided serial port.
	 * @param port The port identifier of the desired serial port.
	 * @throws SerialDeviceInitException When there is a problem connecting to
	 * the serial device.
	 */
	public SerialModule(CommPortIdentifier port) 
	  throws SerialDeviceInitException {
		this(port, 115200);
	}
	
	/**
	 * Create a new SerialModule that corresponds to the serial device with the
	 * specified ID.
	 * @param devId The ID of the desired serial device.
	 * @param portSpeed The baudrate of the serial port.
	 * @throws SerialDeviceInitException When there is a problem connecting to
	 * the serial device.
	 */
	public SerialModule(SerialDeviceID devId, int portSpeed) throws SerialDeviceInitException {
		this(SerialPortMgmt.getPortByDeviceId(devId), portSpeed);
	}
	
	/**
	 * Create a new SerialModule that corresponds to the serial device on the
	 * specified serial port.
	 * @param portName The text name of the desired serial port.
	 * @param portSpeed The baudrate of the serial port.
	 * @throws SerialDeviceInitException When there is a problem connecting to
	 * the serial device.
	 */
	public SerialModule(String portName, int portSpeed) throws SerialDeviceInitException {
		this(getPortIdentifier(portName), portSpeed);
	}
	
	/**
	 * Create a new SerialModule that corresponds to the serial device on the
	 * provided serial port.
	 * @param port The port identifier of the desired serial port.
	 * @param portSpeed The baudrate of the serial port.
	 * @throws SerialDeviceInitException When there is a problem connecting to
	 * the serial device.
	 */
	public SerialModule(CommPortIdentifier port, int portSpeed) {
		if (port == null) {
			throw new IllegalArgumentException("The provided port is null.");
		}
		this.port = port;
		this.portSpeed = portSpeed;
	}
	
	public void begin() throws SerialDeviceInitException {
		initComms(20, 100);
	}
	
	public void begin(int rxRate, int txRate) throws SerialDeviceInitException {
		initComms(rxRate, txRate);
	}
	
	/**
	 * Initiate the connection and start the communications with the serial
	 * device.
	 * @param port The port identifier of the desired serial port.
	 * @param speed The desired baud rate for the serial port.
	 * @throws SerialDeviceInitException When there is a problem connecting to
	 * the serial device.
	 */
	private void initComms(int rxRate, int txRate)
	  throws SerialDeviceInitException {
		
		try {
			// Ensure the port is not already in use
			if (port.getPortType() != CommPortIdentifier.PORT_SERIAL) {
				throw new IllegalArgumentException(
				  "The provided port is not a serial port: " + port.getName());
			}
			
			// Ensure the port is not already in use
			if (port.isCurrentlyOwned()) {
				throw new SerialDeviceInitException(
				  "Port is currently in use: " + port.getName());
			}
			
			// Open the port
			CommPort commPort = port.open(
			  getClass().getName(), 2000);
			
			// Ensure the port is a serial port
			if (!(commPort instanceof SerialPort)) {
				throw new IllegalArgumentException(
				  "The provided port is not a serial port: " + port.getName());
			}
			
			// Set the serial port parameters
			serialPort = (SerialPort) commPort;
			serialPort.setSerialPortParams(
			  portSpeed,
			  SerialPort.DATABITS_8,
			  SerialPort.STOPBITS_1,
			  SerialPort.PARITY_NONE
			);
			
			// Get the input and output streams
			ins = serialPort.getInputStream();
			ins.skip(ins.available());
			outs = serialPort.getOutputStream();
			
		} catch (PortInUseException ex) {
			throw new SerialDeviceInitException(ex,
			  "Port is currently in use: " + port.getName());
		} catch (UnsupportedCommOperationException ex) {
			throw new SerialDeviceInitException(ex,
			  "Cannot set the serial port parameters: " + port.getName());
		} catch (IOException ex) {
			throw new SerialDeviceInitException(ex,
			  "Cannot get the input/output streams: " + port.getName());
		}
		
		// Create a thread to read input changes
		readingThread = new SerialLoopingThread(
		  "Reading from " + port.getName(), 0, rxRate) {
			public void mainLoop() {
				try {
					readUpdatesFromDevice();
				} catch (IOException e) {
					System.err.println("Error reading.");
				}
			}
		};
		readingThread.start();
		
		// Create a thread to write output changes
		writingThread = new SerialLoopingThread(
		  "Writing to " + port.getName(), 1000, txRate) {
			public void mainLoop() {
				try {
					sendUpdatesToDevice();
				} catch (IOException e) {
					System.err.println("Error writing.");
				}
			}
		};
		writingThread.start();
	}
	
	/**
	 * Read a short (2-byte) integer value from the input stream. 
	 * @return The short integer value.
	 * @throws IOException When there is a problem reading from the serial port. 
	 */
	protected int readShort() throws IOException {
		return (ins.read() << 8) + ins.read();
	}
	
	/**
	 * Read a byte value from the input stream. 
	 * @return The byte value.
	 * @throws IOException When there is a problem reading from the serial port. 
	 */
	protected int readByte() throws IOException {
		return ins.read();
	}
	
	/**
	 * Check the number of bytes available for reading from the serial port. 
	 * @return The number of bytes.
	 * @throws IOException When there is a problem reading from the serial port. 
	 */
	protected int bytesAvailable() throws IOException {
		return ins.available();
	}
	
	/**
	 * Write the provided byte to the serial port.
	 * @param b The byte to write.
	 * @throws IOException When there is a problem writing to the serial port.
	 */
	protected void writeByte(int b) throws IOException {
		outs.write(b);
	}
	
	/**
	 * Write the provided bytes to the serial port.
	 * @param b The bytes to write.
	 * @throws IOException When there is a problem writing to the serial port.
	 */
	protected void writeBytes(byte[] b) throws IOException {
		outs.write(b);
	}
	
	/**
	 * Force an update of the serial device (send it a message).
	 */
	public void updateDevice() {
		try {
			sendUpdatesToDevice();
		} catch (IOException e) {
			System.err.println("Error writing to output stream.");
			e.printStackTrace();
		}
	}
	
	/**
	 * Stop the communications to the serial device.
	 */
	public void stopReading() {
		readingThread.stopLoopThread();
		try {
			ins.close();
		} catch (IOException e) {
		}
	}

	/**
	 * Stop the communications from the serial device.
	 */
	public void stopWriting() {
		writingThread.stopLoopThread();
		try {
			outs.close();
		} catch (IOException e) {
		}
	}

	/**
	 * Stop the communications and close the connection to the serial device.
	 */
	public void close() {
		if (closed) {
			return;
		}
		stopReading();
		stopWriting();
		serialPort.close();
		closed = true;
	}
	
	/**
	 * Get whether or not the serial module has been closed. 
	 * @return Whether or not the serial module has been closed.
	 */
	public boolean isClosed() {
		return closed;
	}
	
	/**
	 * @return A text representation of the port.
	 */
	public String getPort() {
		return port.getName();
	}
	
	/**
	 * Get the port identifier of the specified port. 
	 * @param portName The textual name of the desired port.
	 * @return A port identifier object representing the desired port.
	 * @throws SerialDeviceInitException When there is a problem getting the
	 * port identifier of the specified port.
	 */
	private static CommPortIdentifier getPortIdentifier(String portName)
	  throws SerialDeviceInitException {
		try {
			return CommPortIdentifier.getPortIdentifier(portName);
		} catch (NoSuchPortException ex) {
			throw new SerialDeviceInitException(ex,
			  "Cannot find the specified serial port: " + portName);
		}
	}

	/**
	 * Read the updates from the device and update the child classes member
	 * variables and internal state.
	 * @throws IOException When there is a problem reading from the serial
	 * device. 
	 */
	protected abstract void readUpdatesFromDevice() throws IOException;
	
	/**
	 * Send updates to the device using the child classes member variables or
	 * internal state.
	 * @throws IOException When there is a problem writing to the serial device.
	 */
	protected abstract void sendUpdatesToDevice() throws IOException;
}
